﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Net.Security;
using System.Threading;

namespace P3SS.Networking
{
	/// <summary>
	/// Performs underlying interactions with servers (like a TELNET protocol)
	/// </summary>
	public class InnerProtocol
	{
		#region Constants
		/// <summary>
		/// Size of the read buffer
		/// </summary>
		private const int BUFFER_SIZE = 131072;
		/// <summary>
		/// Number of times an empty read can occur before giving up
		/// </summary>
		private const int EMPTY_READ_THRESHOLD = 2;
		/// <summary>
		/// Milliseconds NetworkStream waits before a Read operation timeouts
		/// </summary>
		private const int NETWORK_READ_TIMEOUT = 5000;
		/// <summary>
		/// Maximum number of recoverable errors to try to recover from
		/// </summary>
		private const int MAX_RECOVERABLE_ERRORS = 5;
		#endregion
		#region Fields
		private TcpClient tcpClient;
		private NetworkStream networkStream;
		private SslStream sslStream;
		private string server;
		private int port;
		private bool useSSL;
		#endregion
		#region Constructor
		/// <summary>
		/// Default Constructor. Connects after construction.
		/// </summary>
		/// <param name="inServer">Server hostname to connect to</param>
		/// <param name="inPort">Servers port to connect to</param>
		/// <param name="inUseSSL">Connect using SSL</param>
		public InnerProtocol(string inServer, int inPort, bool inUseSSL)
		{
			server = inServer;
			port = inPort;
			tcpClient = new TcpClient(inServer, inPort);
			tcpClient.ReceiveBufferSize = BUFFER_SIZE;
			tcpClient.SendBufferSize = BUFFER_SIZE;
			//tcpClient.NoDelay = true;
			//tcpClient.Client.Blocking = true;
			networkStream = tcpClient.GetStream();
			networkStream.ReadTimeout = NETWORK_READ_TIMEOUT;
			if (inUseSSL)
			{
				UseSSL();
			}
		}
		#endregion
		#region Properties
		/// <summary>
		/// Is the connection closed
		/// </summary>
		public bool Connected
		{
			get
			{ return tcpClient.Connected; }
		}
		#endregion
		#region Private Properties
		private Stream myStream
		{
			get
			{
				if (useSSL)
				{
					return sslStream;
				}
				else
				{
					return networkStream;
				}
			}
		}
		#endregion
		#region Public Methods
		/// <summary>
		/// Flushes the stream, closes the connection and disposes all resources used
		/// </summary>
		public void Close()
		{
			myStream.Flush();
			myStream.Close();
			myStream.Dispose();
			tcpClient.Close();
		}

		/// <summary>
		/// Writes a line to the stream
		/// </summary>
		/// <param name="data">Data to write. \r\n automatically appended</param>
		public void WriteLine(string data)
		{
			Write(string.Format("{0}\r\n", data));
		}

		/// <summary>
		/// Writes data to the stream
		/// </summary>
		/// <param name="data">Data to write</param>
		public void Write(string data)
		{
			byte[] sendBytes = Encoding.UTF8.GetBytes(data);

			for (int i = 0; i <= sendBytes.Length / BUFFER_SIZE; i++)
			{
				bool dataSentSuccess = true;

				do
				{
					try
					{
						int dataSizeToSend = ((i + 1) * BUFFER_SIZE > sendBytes.Length) ? sendBytes.Length - i * BUFFER_SIZE : BUFFER_SIZE;
						myStream.Write(sendBytes, i * BUFFER_SIZE, dataSizeToSend);
						dataSentSuccess = true;
					}
					catch (IOException ex)
					{
						if ((ex.InnerException != null) && (ex.InnerException is SocketException) && (((SocketException)ex.InnerException).ErrorCode == 10035))
						{
							// Socket Temporarily unavailable, retry
							Thread.Sleep(100);
							dataSentSuccess = false;
						}
						else
						{
							throw ex;
						}
					}
				} while (!dataSentSuccess);
			}
		}

		/// <summary>
		/// Reads a line of command (until \r\n) from the stream
		/// </summary>
		public string ReadLine()
		{
			string output = "";
			byte[] buffer;
			int numberOfBytesRead = 0;
			bool temporaryErrorOccured = false;
			int numberOfEmptyReads = 0;

			do
			{
				buffer = new byte[BUFFER_SIZE];
				numberOfBytesRead = 0;
				temporaryErrorOccured = false;
				try
				{
					numberOfBytesRead = myStream.Read(buffer, 0, BUFFER_SIZE);
					output += Encoding.ASCII.GetString(buffer, 0, numberOfBytesRead);
					if (numberOfBytesRead > 0)
					{
						numberOfEmptyReads = 0;
					}
					else
					{
						numberOfEmptyReads++;
					}
				}
				catch (IOException ex)
				{
					if ((ex.InnerException != null) && (ex.InnerException is SocketException) && (((SocketException)ex.InnerException).ErrorCode == 10035))
					{
						// Temporary error
						tcpClient.Client.Blocking = true;
						temporaryErrorOccured = true;
						numberOfEmptyReads++;
					}
					else
					{
						throw ex;
					}
				}
			} while (((temporaryErrorOccured) || (!output.EndsWith("\r\n"))) && (numberOfEmptyReads < EMPTY_READ_THRESHOLD));

			return output;
		}

		/// <summary>
		/// Reads lines of command until condition is matched
		/// </summary>
		/// <param name="condition">Data the final line must contain (excluding \r\n)</param>
		public string ReadUntil(string condition)
		{
			string output = "";
			int numberOfEmptyReads = 0;

			do
			{
				string lineRead = ReadLine();
				output += lineRead;

				if (lineRead.Length > 0)
				{
					numberOfEmptyReads = 0;
				}
				else
				{
					numberOfEmptyReads++;
				}
			} while ((numberOfEmptyReads < EMPTY_READ_THRESHOLD) && (!output.EndsWith(condition + "\r\n")));

			return output;
		}

		/// <summary>
		/// Reads data from the stream until there is nothing left
		/// </summary>
		/// <returns></returns>
		public string ReadUntilEnd()
		{
			string output = "";
			int emptyReads = 0;

			do
			{
				string receivedData = innerReadUntilEmpty();
				output += receivedData;

				if (receivedData == "")
				{
					emptyReads++;
				}
			} while (emptyReads < EMPTY_READ_THRESHOLD);

			return output;
		}

		/// <summary>
		/// Writes command to the stream, then reads a line back
		/// </summary>
		/// <param name="command">Data to write to the stream (\r\n automatically appended)</param>
		public string WriteCommand(string command)
		{
			return WriteCommand(command, true);
		}

		/// <summary>
		/// Writes command to the stream, then reads a line back
		/// </summary>
		/// <param name="command">Data to write to the stream (\r\n appending optional)</param>
		/// <param name="appendNewLine">Append the new line characters to command?</param>
		public string WriteCommand(string command, bool appendNewLine)
		{
			int numErrors = 0;
			bool sendSuccess = false;
			string output = "";

			while (!sendSuccess)
			{
				try
				{
					if (appendNewLine)
					{
						WriteLine(command);
					}
					else
					{
						Write(command);
					}
					output = ReadLine();
					sendSuccess = true;
				}
				catch (IOException ex)
				{
					if ((numErrors < MAX_RECOVERABLE_ERRORS) && (ex.InnerException != null) && (ex.InnerException is SocketException) && (((SocketException)ex.InnerException).ErrorCode == 10060))
					{
						tcpClient.Client.Blocking = true;
						System.Diagnostics.Debug.WriteLine("Error #" + numErrors);
						numErrors++;
					}
					else
					{
						throw ex;
					}
				}
			}

			return output;
		}

		/// <summary>
		/// Writes command to the stream, then reads until condition matched
		/// </summary>
		/// <param name="command">Data to write to the stream (\r\n automatically appended)</param>
		/// <param name="condition">Data the final line must contain (excluding \r\n)</param>
		public string WriteCommand(string command, string condition)
		{
			int numErrors = 0;
			bool sendSuccess = false;
			string output = "";

			while (!sendSuccess)
			{
				try
				{
					WriteLine(command);
					output = ReadUntil(condition);
					sendSuccess = true;
				}
				catch (IOException ex)
				{
					if ((numErrors < MAX_RECOVERABLE_ERRORS) && (ex.InnerException != null) && (ex.InnerException is SocketException) && (((SocketException)ex.InnerException).ErrorCode == 10060))
					{
						tcpClient.Client.Blocking = true;
						System.Diagnostics.Debug.WriteLine("Error #" + numErrors);
						numErrors++;
					}
					else
					{
						throw ex;
					}
				}
			}

			return output;
		}

		/// <summary>
		/// Encrypts the data stream
		/// </summary>
		public void UseSSL()
		{
			if (!useSSL)
			{
				useSSL = true;
				sslStream = new SslStream(networkStream, false);
				sslStream.AuthenticateAsClient(server);
			}
		}
		#endregion
		#region Private Methods
		private string innerReadUntilEmpty()
		{
			string output = "";
			byte[] buffer;
			int numberOfBytesRead;
			bool isError = false;

			do
			{
				numberOfBytesRead = 0;
				buffer = new byte[BUFFER_SIZE];
				try
				{
					numberOfBytesRead = myStream.Read(buffer, 0, BUFFER_SIZE);
					output += Encoding.ASCII.GetString(buffer, 0, numberOfBytesRead);
				}
				catch (SocketException ex)
				{
					if (ex.ErrorCode != 10035)
					{
						isError = true;
					}
				}
				catch
				{
					isError = true;
				}
			} while (!isError);

			return output;
		}
		#endregion
	}
}
